package net.sf.fc.io;

import static java.nio.file.FileVisitResult.CONTINUE;
import static java.nio.file.FileVisitResult.SKIP_SUBTREE;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.Arrays;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.io.filefilter.IOFileFilter;

import net.sf.fc.io.ch.FileCopyHelper;
import net.sf.fc.io.ch.FileCopyHelper.Action;
import net.sf.fc.io.event.FileCopyEvent.CopyStatus;
import net.sf.fc.io.event.FileCopyEvent.Level;

/**
*
* Class that facilitates the copying of files.
* <p>
* This class enables copying of files and directories. It uses an extension to
* {@link org.apache.commons.io.filecopyhelper.FileCopyHelper} to define behavior
* for handling destination files that exist, exception handling, and reporting
* of copy status. The <tt>FileCopyHelper</tt> instance also provides a method
* to cancel copy operations after they have started. The default <tt>FileCopyHelper</tt>
* instance that is used by <tt>FileCopier</tt> is
* {@link org.apache.commons.io.filecopyhelper.SafeFileCopyHelper}, which does
* not copy any files that already exist in the destination directory. Other
* FileCopyHelper objects that are available include
* {@link org.apache.commons.io.filecopyhelper.OverwriteFileCopyHelper} and
* {@link org.apache.commons.io.filecopyhelper.RenameAppendTimestampFileCopyHelper}.
* Clients can extend <tt>FileCopyHelper</tt> to customize behavior. This class
* is immutable, so it is thread-safe once it is built. It uses {@link FileCopier.Builder}
* to build an instance of <tt>FileCopier</tt>.
* <p>
* <tt>FileCopier</tt> also allows clients to define behavior for copying directories.
* A client can instruct <tt>FileCopier</tt> to "flatten" directories. This means
* that, when directories are copied to the destination directories, the source
* directory itself is not created. Only the contents of the directory are copied.
* Directory flattening can be set for all directories, for levels of directories,
* or by name. By default, <tt>FileCopier</tt> does not flatten any directories.
* <p>
* <tt>FileCopier</tt> clients can also merge directories if they already exist
* in the destination directory. This means that the contents of the source
* directory will be merged with the contents of the destination directory of the
* same name. The <tt>FileCopyHelper</tt> object would define behavior for any files
* in the destination directory that already exist (overwrite, rename, etc).
* Merging can be set for all directories, for levels of directories, or by name.
* If directories are not merged, <tt>FileCopier</tt> will apply the rule defined
* by its <tt>FileCopyHelper</tt> object when it encounters a destination directory
* that already exists. For instance, if <tt>FileCopier</tt> is using
* {@link org.apache.commons.io.filecopyhelper.RenameAppendFileCopyHelper} and it
* encounters a destination directory that already exists, it would rename the
* destination directory and then create a new directory with the original name.
* By default, <tt>FileCopier</tt> does not merge any directories.
*
* @author David Armstrong
*/
public final class FileCopier {

    /**
     * Enum that is used by {@link FileCopier#copyFile(java.io.File, java.util.Map)}.
     * This enum is needed by this method to tell it how to treat destinations
     * that don't exist; as a file or a directory.
     */
    public static enum FileType {
        File, Directory
    }

    /**
     * The default Options object that FileCopier will use if an Options object is
     * not passed in to the copy method called.
     */
    private final Options defaultOptions;

    /**
     * State captures the state of this FileCopier object in relation to copying.
     * If IDLE, it is not making copies. If COPY, it is making copies. If CANCEL,
     * the cancel method was called while it was copying and it will cancel
     * subsequent copies as soon as it checks the state.
     */
    private final AtomicInteger state;

    /**
     * IDLE means the FileCopier object is not making copies.
     */
    private static final int IDLE = 1;

    /**
     * COPY means the FileCopier object is making copies.
     */
    private static final int COPY = 0;

    /**
     * CANCEL means the FileCopier object's cancel method got called while it was
     * making copies. It will cancel subsequent copies when it checks the state.
     */
    private static final int CANCEL = -1;

    /**
     * Constructor that takes an Options argument which will be used as the default
     * defaultOptions for copy operations by this FileCopier object.
     * @param defaultOptions
     */
    public FileCopier(final Options options) {
        defaultOptions = options;
        state = new AtomicInteger(IDLE);
    }

    /**
     * Default constructor that uses the default argument for its Options object.
     */
    public FileCopier() {
        this(Options.createDefaultOptions());
    }

    /**
     * This method will return a new FileCopier object that uses the Options instance
     * passed in as its default Options.
     * @param options
     * @return a new instance of FileCopier
     */
    public FileCopier setOptions(Options options) {
        return new FileCopier(options);
    }

    /**
     * This method returns the default Options object in use by this FileCopier instance.
     * @return the default Options
     */
    public Options getOptions() {
        return defaultOptions;
    }

    public boolean copyFileToDirectory(File srcFile, File dstDir) {
        boolean copied;
        state.set(COPY);
        try {
            copied = copyFileToDirectory(srcFile, dstDir, defaultOptions);
        } finally {
            state.set(IDLE);
        }
        return copied;
    }

    private boolean copyFileToDirectory(File srcFile, File dstDir, Options options) {

        if (dstDir == null) {
            throw new IllegalArgumentException("Destination must not be null");
        }
        if (dstDir.exists() && dstDir.isDirectory() == false) {
            throw new IllegalArgumentException("Destination '" + dstDir + "' is not a directory");
        }
        return copyFile(srcFile, new File(dstDir, srcFile.getName()), options);
    }

    /**
     * Method that copies the specified source file to the specified destination
     * file. If the destination directory does not exist, it is created. If the
     * destination file exists, the FileCopyHelper object determines the action
     * to take. <tt>FileCopier</tt>'s preserveFileDate boolean value determines
     * if the source file's last modified date should be applied to the copied
     * file. If an <tt>IOException</tt> or {@link FileCopyException} is thrown,
     * the <tt>FileCopyHelper</tt> object determines the action to take.
     *
     * @param srcFile an existing file to copy, must not be <code>null</code>
     * @param destFile the new file, must not be <code>null</code>
     *
     * @return true if copy was successful
     */
    public boolean copyFile(File srcFile, File dstFile) {
        boolean copied;
        state.set(COPY);
        try {
            copied = copyFile(srcFile, dstFile, defaultOptions);
        } finally {
            state.set(IDLE);
        }
        return copied;
    }

    /**
     * Static method that copies a file to another named file. Uses the specified
     * <tt>FileCopyHelper</tt> object. The specified boolean preserveFileDate
     * determines if the last modified date of copied files are preserved.
     * @param srcFile the source file, must not be <code>null</code>
     * @param destFile the destination file, must not be <code>null</code>
     * @param fileCopyHelper FileCopyHelper object
     * @param preserveFileDate preserve file date boolean
     * @return true if copy was successful
     */
    private boolean copyFile(File srcFile, File dstFile, Options options) {

        if(srcFile == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstFile == null) {
            throw new IllegalArgumentException("Destination must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        return doCopyFile(srcFile, dstFile, options);
    }

    private boolean doCopyFile(File srcFile, File dstFile, Options options) {
        // Action is an enum defined in FileCopyHelper. It is the return value of handleExistingFile. If handleExistingFile returns
        // CONTINUE, it is okay for FileCopier to copy the file. If it returns SKIP, FileCopier should skip copying the file. If it
        // returns ABEND, FileCopyHelper tried to take an action to enable FileCopier to copy the file, but it failed.
        Action action = Action.CONTINUE;
        Throwable t = null;
        FileCopyHelper fileCopyHelper = options.getFileCopyHelper();

        try {
            if(srcFile.exists() == false) {
                throw new FileNotFoundException("Source '" + srcFile + "' does not exist");
            }
            if (srcFile.isDirectory()) {
                throw new IOException("Source '" + srcFile + "' exists but is a directory");
            }
            if(dstFile.isDirectory()) {
                throw new IOException("Destination '" + dstFile + " is a directory");
            }
            File dstDir = dstFile.getParentFile();
            if (dstDir != null && dstDir.exists() == false) {
                if (dstDir.mkdirs() == false) {
                    throw new IOException("Destination '" + dstDir + "' directory cannot be created");
                }
            }
            if(dstFile.exists()) {
                // action will be either CONTINUE, SKIP, or ABEND. CONTINUE or SKIP is not considered an error.
                action = fileCopyHelper.handleExistingFile(srcFile, dstFile);
            }

            // If action is CONTINUE, handleExistingFile was never called or it was and it gave us the go-ahead to do the copy.
            if(action.equals(Action.CONTINUE)) {
                try {
                    Files.copy(srcFile.toPath(), dstFile.toPath(), options.getFileCpOptions());
                } catch(IOException e) {
                    throw new FileCopyException(e, srcFile, dstFile);
                }
            }
        } catch(IOException e) {
            action = Action.ABEND;
            t = e;
        } catch(FileCopyException e) {
            action = fileCopyHelper.handleFileCopyException(e) ? Action.CONTINUE : Action.ABEND;
            t = e;
        }

        // If action is SKIP, the reason for the skip was already reported, so no need to report anything.
        if(action.equals(Action.CONTINUE)) {
            fileCopyHelper.reportCopyEvent(srcFile, dstFile, CopyStatus.COPY_COMPLETE, Level.INFO, dstFile.length(), t);
        } else if(action.equals(Action.ABEND)) {
            fileCopyHelper.reportCopyEvent(srcFile, dstFile, CopyStatus.COPY_FAILED, Level.ERROR, 0, t);
        }

        // This method returns true if the file was copied. The file was copied only if action is CONTINUE, so return true
        // only if action is CONTINUE.
        return action.equals(Action.CONTINUE);
    }

    public int copyDirectory(File srcDir, File dstDir) {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyDirectory(srcDir, dstDir, defaultOptions);
        } finally {
            state.set(IDLE);
        }
        return copyCt;
    }

    private int copyDirectory(File srcDir, File dstDir, Options options) {

        if(srcDir == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstDir == null) {
            throw new IllegalArgumentException("Destination must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;

        try {

            if(srcDir.exists() == false) {
                throw new FileNotFoundException("Source '" + srcDir + "' does not exist");
            }
            if(srcDir.isDirectory() == false) {
                throw new IOException("Source '" + srcDir + "' exists but is not a directory");
            }

            if(dstDir.exists() && dstDir.isFile()) {
                throw new IOException("Source '" + srcDir + "' is a directory, but Destination '" + dstDir + "' is not");
            }

            // The destination either does not exist or exists and is a directory.
            Path srcPath = srcDir.toPath();
            Path dstPath = getDstPath(srcDir, dstDir, options);
            TreeCopier tc = new TreeCopier(this, srcPath, dstPath, options);
            EnumSet<FileVisitOption> opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
            Files.walkFileTree(srcPath, opts, Integer.MAX_VALUE, tc);
            copyCt = tc.getCopyCt();

        } catch(IOException e) {
            options.getFileCopyHelper().reportCopyEvent(srcDir, dstDir, CopyStatus.COPY_FAILED, Level.ERROR, 0, e);
        }

        return copyCt;
    }

    /**
     * Static method that copies a source directory to multiple destination directories.
     * @param srcDir source file, must not be <code>null</code>
     * @param destDirs <tt>List</tt> of destination directories, must not be <code>null</code>
     * @param options <tt>Options</tt> object to use for each copy operation must not be <code>null</code>
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    public int copyDirectory(File srcDir, List<File> dstDirs) throws FileCopyCancelException {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyDirectory(srcDir, dstDirs, defaultOptions);
        } finally {
            state.set(IDLE);
        }
        return copyCt;
    }

    private int copyDirectory(File srcDir, List<File> dstDirs, Options options) throws FileCopyCancelException {

        if(srcDir == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstDirs == null) {
            throw new IllegalArgumentException("Destination list must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;
        for(File dir: dstDirs) {
            copyCt += copyDirectory(srcDir, dir, options);
            if(state.get() == CANCEL) throw new FileCopyCancelException("Copy operations cancelled.");
        }

        return copyCt;
    }

    /**
     * Method that copies a source directory to multiple destination directories.
     * @param srcDir source file, must not be <code>null</code>
     * @param dests <tt>Map</tt> of Options and destination directories, must not be <code>null</code>
     * If the Options key is null, the default options object is used. If the destination directory
     * is null, a IllegalArgumentException is thrown.
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
   public int copyDirectory(File srcDir, Map<Options, List<File>> dstMap) throws FileCopyCancelException {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyDirectory(srcDir, dstMap, defaultOptions);
        } finally {
            state.set(IDLE);
        }
        return copyCt;
    }

    private int copyDirectory(File srcDir, Map<Options, List<File>> dstMap, Options options) throws FileCopyCancelException {
        if(srcDir == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstMap == null) {
            throw new IllegalArgumentException("Destination must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;
        Set<Options> optionsSet = dstMap.keySet();
        for(Options o: optionsSet) {
            List<File> destDirs = dstMap.get(o);
            // If options is null, set it to options
            if(o == null) o = options;

            copyCt += copyDirectory(srcDir, destDirs, o);
            if(state.get() == CANCEL) throw new FileCopyCancelException("Copy operations cancelled.");
        }

        return copyCt;
    }

    public int copyFileToFiles(File srcFile, List<File> dstFiles) throws FileCopyCancelException {
        int copyCt = 0;
        state.set(COPY);

        try {
            copyCt = copyFileToFiles(srcFile, dstFiles, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    private int copyFileToFiles(File srcFile, List<File> dstFiles, Options options) throws FileCopyCancelException {
        if(dstFiles == null) {
            throw new IllegalArgumentException("Destination List must not be null");
        }

        int copyCt = 0;
        for(File dstFile: dstFiles) {
            if(copyFile(srcFile, dstFile, options)) copyCt++;
            if(state.get() == CANCEL) throw new FileCopyCancelException("Copy operations cancelled.");
        }

        return copyCt;
    }

    public int copyFileToFiles(File srcFile, Map<Options, List<File>> dstMap) {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyFileToFiles(srcFile, dstMap, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    private int copyFileToFiles(File srcFile, Map<Options, List<File>> dstMap, Options options) {
        if(dstMap == null) {
            throw new IllegalArgumentException("Destination Map must not be null");
        }
        int copyCt = 0;

        for(Options o: dstMap.keySet()) {
            List<File> dstFiles = dstMap.get(o);
            if(o == null) o = options;
            copyCt += copyFileToFiles(srcFile, dstFiles, o);
        }

        return copyCt;
    }

    /**
     * Method that copies a source file to multiple destination directories.
     * @param srcFile source file, must not be <code>null</code>
     * @param destDirs <tt>List</tt> of destination directories, must not be <code>null</code>
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    public int copyFileToDirectories(File srcFile, List<File> dstDirs) {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyFileToDirectories(srcFile, dstDirs, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    private int copyFileToDirectories(File srcFile, List<File> dstDirs, Options options) {

        if(srcFile == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstDirs == null) {
            throw new IllegalArgumentException("Destination must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;
        for(File destDir: dstDirs) {
            if(copyFileToDirectory(srcFile, destDir, options)) copyCt++;
            if(state.get() == CANCEL) throw new FileCopyCancelException("Copy operations cancelled.");
        }

        return copyCt;
    }

    /**
     * Method that copies a source file to multiple destination directories.
     * @param srcFile source file, must not be <code>null</code>
     * @param destMap <tt>Map</tt> of Options objects and destination directories, must not be <code>null</code>
     * If an Options key for destMap is <code>null</code>, the defaultOptions object for this FileCopier instance
     * will be used.
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    public int copyFileToDirectories(File srcFile, Map<Options, List<File>> dstMap) {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyFileToDirectories(srcFile, dstMap, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    private int copyFileToDirectories(File srcFile, Map<Options, List<File>> dstMap, Options options) {

        if(srcFile == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstMap == null) {
            throw new IllegalArgumentException("Destination map must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;
        for(Options o: dstMap.keySet()) {
            List<File> destDirs = dstMap.get(o);
            if(o == null) o = options;

            copyCt += copyFileToDirectories(srcFile, destDirs, o);
            if(state.get() == CANCEL) throw new FileCopyCancelException("Copy operations cancelled.");
        }

        return copyCt;
    }

    /**
     * Method that copies multiple source files to a destination directory.
     * @param srcFiles <tt>List</tt> of source files, must not be <code>null</code>
     * @param destDir destination directory, must not be <code>null</code>
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    public int copyFilesToDirectory(List<File> srcFiles, File dstDir) throws FileCopyCancelException {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyFilesToDirectory(srcFiles, dstDir, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    private int copyFilesToDirectory(List<File> srcFiles, File dstDir, Options options) throws FileCopyCancelException {

        if(srcFiles == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstDir == null) {
            throw new IllegalArgumentException("Destination must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("FileCopyHelper must not be null");
        }

        int copyCt = 0;
        for(File srcFile: srcFiles) {
            if(copyFileToDirectory(srcFile, dstDir, options)) copyCt++;
            if(state.get() == CANCEL) throw new FileCopyCancelException("Copy operations cancelled.");
        }

        return copyCt;
    }

    public int copyFilesToDirectory(Map<Options, List<File>> srcMap, File dstDir) throws FileCopyCancelException {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyFilesToDirectory(srcMap, dstDir, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    private int copyFilesToDirectory(Map<Options, List<File>> srcMap, File dstDir, Options options) throws FileCopyCancelException {

        if(srcMap == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstDir == null) {
            throw new IllegalArgumentException("Destination must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("FileCopyHelper must not be null");
        }

        int copyCt = 0;
        for(Options o: srcMap.keySet()) {
            List<File> srcFiles = srcMap.get(o);

            if(o == null) o = options;
            copyCt += copyFilesToDirectory(srcFiles, dstDir, o);
            if(state.get() == CANCEL) throw new FileCopyCancelException("Copy operations cancelled.");
        }

        return copyCt;
    }

    /**
     * Method that copies multiple directories to a destination directory.
     * @param srcDirs <tt>List</tt> of source directories, must not be <code>null</code>
     * @param destDir destination directory, must not be <code>null</code>
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    public int copyDirectories(List<File> srcDirs, File dstDir) throws FileCopyCancelException {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyDirectories(srcDirs, dstDir, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    private int copyDirectories(List<File> srcDirs, File dstDir, Options options) throws FileCopyCancelException {

        if(srcDirs == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstDir == null) {
            throw new IllegalArgumentException("Destination must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;
        for(File srcDir: srcDirs) {
            copyCt += copyDirectory(srcDir, dstDir, options);
            if(state.get() == CANCEL) throw new FileCopyCancelException("Copy operations cancelled.");
        }

        return copyCt;
    }

    public int copyDirectories(Map<Options, List<File>> srcMap, File dstDir) throws FileCopyCancelException {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyDirectories(srcMap, dstDir, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    private int copyDirectories(Map<Options, List<File>> srcMap, File dstDir, Options options) throws FileCopyCancelException {

        if(srcMap == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstDir == null) {
            throw new IllegalArgumentException("Destination must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;
        for(Options o : srcMap.keySet()) {
            List<File> srcDirs = srcMap.get(o);
            if(o == null) o = options;

            copyCt += copyDirectories(srcDirs, dstDir, o);
            if(state.get() == CANCEL) throw new FileCopyCancelException("Copy operations cancelled.");
        }

        return copyCt;
    }

    /**
     * Method that copies source directories to destination directories.
     * @param srcDirs <tt>List</tt> of source directories, must not be <code>null</code>
     * @param destDirs <tt>List</tt> of destination directory, must not be <code>null</code>
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    public int copyDirectories(List<File> srcDirs, List<File> dstDirs) throws FileCopyCancelException {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyDirectories(srcDirs, dstDirs, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    private int copyDirectories(List<File> srcDirs, List<File> dstDirs, Options options) throws FileCopyCancelException {

        if(srcDirs == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstDirs == null) {
            throw new IllegalArgumentException("Destination must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;
        for(File dstDir : dstDirs) {
            copyCt += copyDirectories(srcDirs, dstDir, options);
        }

        return copyCt;
    }

    /**
     * Method that copies source directories to destination directories.
     * @param srcMap <tt>Map</tt> of source directories, must not be <code>null</code>
     * @param destDirs <tt>List</tt> of destination directory, must not be <code>null</code>
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    public int copyDirectories(Map<Options, List<File>> srcMap, List<File> dstDirs) throws FileCopyCancelException {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyDirectories(srcMap, dstDirs, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    private int copyDirectories(Map<Options, List<File>> srcMap, List<File> dstDirs, Options options) throws FileCopyCancelException {

        if(srcMap == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstDirs == null) {
            throw new IllegalArgumentException("Destination must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;
        for(Options o : srcMap.keySet()) {
            copyCt += copyDirectories(srcMap.get(o), dstDirs, o != null ? o : options);
        }

        return copyCt;
    }

    /**
     * Method that copies source directories to destination directories.
     * @param srcMap <tt>Map</tt> of source directories, must not be <code>null</code>
     * @param destDirs <tt>List</tt> of destination directory, must not be <code>null</code>
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    public int copyDirectories(List<File> srcDirs, Map<Options, List<File>> dstMap) throws FileCopyCancelException {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyDirectories(srcDirs, dstMap, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    private int copyDirectories(List<File> srcDirs, Map<Options, List<File>> dstMap, Options options) throws FileCopyCancelException {

        if(srcDirs == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstMap == null) {
            throw new IllegalArgumentException("Destination must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;
        for(Options o : dstMap.keySet()) {
            copyCt += copyDirectories(srcDirs, dstMap.get(o), o != null ? o : options);
        }

        return copyCt;
    }

    /**
     * Method that copies source directories to destination directories.
     * @param srcMap <tt>Map</tt> of source directories, must not be <code>null</code>
     * @param dstMap <tt>Map</tt> of destination directory, must not be <code>null</code>
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    public int copyDirectories(Map<Options, List<File>> srcMap, Map<Options, List<File>> dstMap) throws FileCopyCancelException {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyDirectories(srcMap, dstMap, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    private int copyDirectories(Map<Options, List<File>> srcMap, Map<Options, List<File>> dstMap, Options options) throws FileCopyCancelException {

        if(srcMap == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstMap == null) {
            throw new IllegalArgumentException("Destination must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;
        for(Options o : srcMap.keySet()) {
            copyCt += copyDirectories(srcMap.get(o), dstMap, o != null ? o : options);
        }

        return copyCt;
    }

    /**
     * Method that copies multiple source files to multiple destination directories.
     * @param srcFiles <tt>List</tt> of source files, must not be <code>null</code>
     * @param destDirs <tt>List</tt> of destination directories, must not be
     * <code>null</code>
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    public int copyFilesToDirectories(List<File> srcFiles, List<File> dstDirs) throws FileCopyCancelException {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyFilesToDirectories(srcFiles, dstDirs, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    /**
     * Static method that copies multiple source files to multiple destination directories.
     * @param srcFiles <tt>List</tt> of source files, must not be <code>null</code>
     * @param destDirs <tt>List</tt> of destination directories, must not be <code>null</code>
     * @param options the Options object to use
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    private int copyFilesToDirectories(List<File> srcFiles, List<File> dstDirs, Options options) throws FileCopyCancelException {

        if(srcFiles == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstDirs == null) {
            throw new IllegalArgumentException("Destination must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;
        for(File srcFile: srcFiles) {
            for(File destDir: dstDirs) {
                if(copyFileToDirectory(srcFile, destDir, options)) copyCt++;
                if(state.get() == CANCEL) throw new FileCopyCancelException("Copy operations cancelled.");
            }
        }

        return copyCt;
    }

    /**
     * Method that copies multiple source files to multiple destination directories.
     * @param srcMap <tt>Map</tt> of <tt>Options</tt>/<tt>List</tt> of <tt>File</tt> objects, must not be <code>null</code>
     * @param destDirs <tt>List</tt> of destination directories, must not be <code>null</code>
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    public int copyFilesToDirectories(Map<Options,List<File>> srcMap, List<File> dstDirs) throws FileCopyCancelException {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyFilesToDirectories(srcMap, dstDirs, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    /**
     * Static method that copies multiple source files to multiple destination directories.
     * @param srcMap <tt>Map</tt> of <tt>Options</tt>/<tt>List</tt> of <tt>File</tt> objects, must not be <code>null</code>
     * @param destDirs <tt>List</tt> of destination directories, must not be <code>null</code>
     * @param options the default Options object to use
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    private int copyFilesToDirectories(final Map<Options,List<File>> srcMap, final List<File> destDirs, Options options) throws FileCopyCancelException {

        if(srcMap == null) {
            throw new IllegalArgumentException("Source Map must not be null");
        }
        if(destDirs == null) {
            throw new IllegalArgumentException("Destination must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;
        for(Options o: srcMap.keySet()) {
            List<File> srcFiles = srcMap.get(o);
            if(o == null) o = options;
            copyCt += copyFilesToDirectories(srcFiles, destDirs, o);
            if(state.get() == CANCEL) throw new FileCopyCancelException("Copy operations cancelled.");
        }

        return copyCt;
    }

    /**
     * Method that copies multiple source files to multiple destination directories.
     * @param srcFiles <tt>List</tt> of source files, must not be <code>null</code>
     * @param destMap <tt>Map</tt> of <tt>Options</tt>/<tt>List</tt> of <tt>File</tt> objects, must not be <code>null</code>
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    public int copyFilesToDirectories(List<File> srcFiles, Map<Options,List<File>> dstMap) throws FileCopyCancelException {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyFilesToDirectories(srcFiles, dstMap, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    /**
     * Static method that copies multiple source files to multiple destination directories.
     * @param srcFiles <tt>List</tt> of source files, must not be <code>null</code>
     * @param destMap <tt>Map</tt> of <tt>Options</tt>/<tt>List</tt> of <tt>File</tt> objects, must not be <code>null</code>
     * @param options the default Options object to use
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    private int copyFilesToDirectories(List<File> srcFiles, Map<Options,List<File>> dstMap, Options options) throws FileCopyCancelException {

        if(srcFiles == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstMap == null) {
            throw new IllegalArgumentException("Destination Map must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;
        for(Options o: dstMap.keySet()) {
            List<File> dstDirs = dstMap.get(o);
            if(o == null) o = options;
            copyCt += copyFilesToDirectories(srcFiles, dstDirs, o);
            if(state.get() == CANCEL) throw new FileCopyCancelException("Copy operations cancelled.");
        }

        return copyCt;
    }

    /**
     * Method that copies multiple source files to multiple destination directories.
     * @param srcMap <tt>Map</tt> of <tt>Options</tt>/<tt>List</tt> of <tt>File</tt> objects, must not be <code>null</code>
     * @param destMap <tt>Map</tt> of <tt>Options</tt>/<tt>List</tt> of <tt>File</tt> objects, must not be <code>null</code>
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    public int copyFilesToDirectories(final Map<Options,List<File>> srcMap, final Map<Options,List<File>> dstMap) throws FileCopyCancelException {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyFilesToDirectories(srcMap, dstMap, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    /**
     * Static method that copies multiple source files to multiple destination directories.
     * @param srcMap <tt>Map</tt> of <tt>Options</tt>/<tt>List</tt> of <tt>File</tt> objects, must not be <code>null</code>
     * @param destMap <tt>Map</tt> of <tt>Options</tt>/<tt>List</tt> of <tt>File</tt> objects, must not be <code>null</code>
     * @param options the default Options object to use
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    private int copyFilesToDirectories(final Map<Options,List<File>> srcMap, final Map<Options,List<File>> dstMap, Options o3) throws FileCopyCancelException {

        if(srcMap == null) {
            throw new IllegalArgumentException("Source Map must not be null");
        }
        if(dstMap == null) {
            throw new IllegalArgumentException("Destination Map must not be null");
        }
        if(o3 == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;
        for(Options o2: srcMap.keySet()) {
            List<File> srcFiles = srcMap.get(o2);
            if(o2 == null) o2 = o3;
            for(Options o1: dstMap.keySet()) {
                List<File> dstDirs = dstMap.get(o1);
                if(o1 == null) o1 = o2;
                copyCt += copyFilesToDirectories(srcFiles, dstDirs, o1);
                if(state.get() == CANCEL) throw new FileCopyCancelException("Copy operations cancelled.");
            }
        }

        return copyCt;
    }

    /**
     * Method that copies a source file to multiple destination files or directories.
     * Destination files/directories are contained in a <tt>Map</tt> with the
     * <tt>Options</tt> object as the key and a <tt>Map</tt> of FileType/List of Files
     * as the value. The FileType object indicates if the destination File should
     * be treated as a file or a directory. If the destination does not exist,
     * <tt>FileCopier</tt> needs to know if it should create the File as a regular
     * file or a directory.
     * @param srcFile source file, must not be <code>null</code>
     * @param destMap <tt>Map</tt> of <tt>Options</tt>/Map of <tt>FileType</tt>/<tt>List</tt> of <tt>File</tt>
     * pairs, must not be <code>null</code>
     * @return number of files successfully copied
     * @throws FileCopyCancelException
     */
    public int copyFile(File srcFile, Map<FileCopier.FileType,FileLists> dstMap) throws FileCopyCancelException {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyFile(srcFile, dstMap, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    private int copyFile(File srcFile, Map<FileCopier.FileType,FileLists> dstMap, Options options) throws FileCopyCancelException {

        if(srcFile == null) {
            throw new IllegalArgumentException("Source must not be null");
        }
        if(dstMap == null) {
            throw new IllegalArgumentException("Destination must not be null");
        }
        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;

        for(Map.Entry<FileType, FileLists> entry : dstMap.entrySet()) {
            FileType fileType = entry.getKey();
            FileLists dstFileLists = entry.getValue();
            for(FileList dstFileList : dstFileLists) {
                Options o = dstFileList.getOptions();
                if(o == null) o = options;
                List<File> dstFiles = dstFileList.getFiles();
                if(dstFiles != null) {
                    if(fileType == FileCopier.FileType.Directory) {
                        copyCt += srcFile.isDirectory() ? copyDirectory(srcFile, dstFiles, o) : copyFileToDirectories(srcFile, dstFiles, o);
                    } else {
                        copyCt += copyFileToFiles(srcFile, dstFiles, o);
                    }
                    if(state.get() == CANCEL) throw new FileCopyCancelException("Copy operations cancelled.");
                }
            }
        }

        return copyCt;
    }

    /**
     * Method that copies multiple source files to multiple destinations.
     * @param filesMap <tt>Map</tt> of source files and destination directories. The
     * key is the source <tt>File</tt> object. The value is a <tt>Map</tt> of
     * destination files. The key for this map is the file type, File or Directory.
     * The value for this map is a List of file objects.
     * The map object must not be <code>null</code>
     * @return number of files successfully copied.
     * @throws FileCopyCancelException
     */
    public int copyFiles(Map<FileLists,Map<FileType,FileLists>> filesMap) {
        int copyCt = 0;
        state.set(COPY);
        try {
            copyCt = copyFiles(filesMap, defaultOptions);
        } finally {
            state.set(IDLE);
        }

        return copyCt;
    }

    private int copyFiles(Map<FileLists,Map<FileType,FileLists>> filesMap, Options options) {

        if(filesMap == null) {
            throw new IllegalArgumentException("File map must not be null");
        }

        if(options == null) {
            throw new IllegalArgumentException("Options must not be null");
        }

        int copyCt = 0;

        for(Map.Entry<FileLists, Map<FileType, FileLists>> entry : filesMap.entrySet()) {
            FileLists fileLists = entry.getKey();
            Map<FileType, FileLists> dstMap = entry.getValue();
            for(FileList fList : fileLists) {
                Options o = fList.getOptions();
                if(o == null) o = options;
                for(File srcFile : fList) {
                    copyCt += copyFile(srcFile, dstMap, o);
                }
            }
        }

        return copyCt;
    }

    /**
     * Sets the state to CANCEL if the current state is COPY.
     * If the current state is not COPY, no copy is in progress or it
     * already received a CANCEL notification, so it returns false.
     */
    public boolean cancel() {
        return state.compareAndSet(COPY, CANCEL);
    }

    private static Path getDstPath(File srcDir, File dstDir, Options options) {
        IOFileFilter dirFlattenFilter = options.getDirFlattenFilter();
        Path dstPath = dstDir.toPath();

        return options.getMaxDirFlattenLevel() == Options.NO_FLATTEN &&
                (!dirFlattenFilter.accept(srcDir))
                    ? dstPath.resolve(srcDir.toPath().getFileName())
                    : dstPath;
    }

    private static class TreeCopier implements FileVisitor<Path> {

        private final Path src;
        private final Path dst;
        private final Options options;
        private final int maxDirFlattenLvl;
        private int copyCt;
        private final FileCopier fileCopier;

        public TreeCopier(FileCopier fileCopier, Path src, Path dst, Options options) {
            this.src = src;
            this.dst = dst;
            this.options = options;
            this.fileCopier = fileCopier;
            maxDirFlattenLvl = initMaxDirFlattenLvl(options.getMaxDirFlattenLevel());
            copyCt = 0;
        }

        private int initMaxDirFlattenLvl(int realMaxDirFlattenLvl) {
            if(realMaxDirFlattenLvl == Options.NO_FLATTEN || realMaxDirFlattenLvl == Options.FLATTEN_ALL_DIRS) {
                return realMaxDirFlattenLvl;
            }
            if(realMaxDirFlattenLvl == 1) {
                return Options.NO_FLATTEN;
            }
            return --realMaxDirFlattenLvl;
        }

        @Override
        public FileVisitResult preVisitDirectory(Path dir,
                BasicFileAttributes attrs) throws IOException {
            if(fileCopier.state.get() == CANCEL) {
                throw new FileCopyCancelException("Copy operations cancelled");
            }

            FileVisitResult result = CONTINUE;

            int curDirLvl = getCurrentDirLvl(dir.normalize());
            boolean copyDir;
            if(copyDir = toCopyDir(dir, curDirLvl)) {
                Path relativeFlatSrc = getRelativeFlattenedPath(src.relativize(dir), options.getDirFlattenFilter());
                Path dstDirPath = dst.resolve(relativeFlatSrc);

                // See if we are supposed to flatten this directory.  Flatten means do not create a directory in
                // the destination directory with the same name as the source directory and to only copy
                // the contents of the source directory.
                if(!toFlattenDir(dir, curDirLvl)) {
                    // This boolean determines if we need to create the new destination directory.
                    boolean makeNewDir = false;
                    File dstDirFile = dstDirPath.toFile();
                    if(dstDirFile.exists()) {
                        // See if we are supposed to merge this directory. Merge means, if the destination directory
                        // already exists, do not overwrite or rename the directory, but merge its contents
                        // with the source directory.
                        // If we are not supposed to merge this directory OR the newDestDir file
                        // is NOT a directory, we need to handle the existence of this file.
                        if(!toMergeDir(dir, curDirLvl) || !dstDirFile.isDirectory()) {
                            Action action = options.getFileCopyHelper().handleExistingFile(dir.toFile(), dstDirFile);
                            // We should go ahead with copying directory only if action is CONTINUE. If it is SKIP or ABEND,
                            // we should not go forward.
                            copyDir = action.equals(Action.CONTINUE);
                            // We handled the existence of the new destination directory. If this was successful,
                            // one of the possibilities is that it renamed the file. In this case, we need to
                            // create the new destination directory, so set makeNewDir to the return value of
                            // handleExistingFile.
                            makeNewDir = copyDir;
                        }
                    } else {
                        makeNewDir = true;
                    }

                    if(makeNewDir) {
                        try {
                            // Files.copy will fail if the parent of the directory we try to create does not exist.
                            // Try to create any sub-directories of the destination directory that don't exist.
                            File dstParentDir = dstDirPath.toFile().getParentFile();
                            if(!dstParentDir.exists() && !dstParentDir.mkdirs()) {
                                throw new IOException("Destination '" + dstParentDir + "' directory cannot be created");
                            }
                            Files.copy(dir, dstDirPath, options.getDirCpOptions());
                        } catch(FileAlreadyExistsException ignored) {
                            // It doesn't matter if we get this because we were trying to create the directory and it already exists.
                        } catch(IOException e) {
                            options.getFileCopyHelper().reportCopyEvent(null, null, CopyStatus.COPY_FAILED, Level.ERROR, 0, e);
                            result = SKIP_SUBTREE;
                        }
                    }
                }
            } else {
                result = SKIP_SUBTREE;
            }
            return result;
        }

        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                throws IOException {
            Path relativeFlatSrc = getRelativeFlattenedPath(src.relativize(file.getParent()), options.getDirFlattenFilter());
            Path dstDirPath = dst.resolve(relativeFlatSrc);
            Path dstFile = dstDirPath.resolve(file.getFileName());
            if(toCopyFile(file) && fileCopier.doCopyFile(file.toFile(), dstFile.toFile(), options)) copyCt++;
            return CONTINUE;
        }

        public int getCopyCt() {
            return copyCt;
        }

        @Override
        public FileVisitResult visitFileFailed(Path file, IOException exc)
                throws IOException {
            // TODO Auto-generated method stub
            return null;
        }

        @Override
        public FileVisitResult postVisitDirectory(Path dir, IOException exc)
                throws IOException {
            if(exc == null && options.isCopyAttributes()) {
                Path newdir = dst.resolve(src.relativize(dir));
                try {
                    FileTime time = Files.getLastModifiedTime(dir);
                    Files.setLastModifiedTime(newdir, time);
                } catch(IOException e) {
                    //TODO: Handle this.
                }
            }
            return CONTINUE;
        }

        private int getCurrentDirLvl(Path dir) {
            return getCurrentDirLvl(dir, 0);
        }

        private int getCurrentDirLvl(Path dir, int lvl) {
            return dir != null && !dir.equals(src) ? getCurrentDirLvl(dir.getParent(), lvl+1) : lvl;
        }

        private Path getRelativeFlattenedPath(Path relativePath, IOFileFilter dirFlattenFilter) {
            Path relativeFlattenedPath = getRelativeFlattenedPath(relativePath);
            Deque<String> dirDeque = new LinkedList<>();
            getRelativeFlattenedPath(relativeFlattenedPath.toFile(), dirFlattenFilter, dirDeque);
            relativeFlattenedPath = Paths.get("");
            if(dirDeque.size() > 0) {
                for(String dir: dirDeque) {
                    relativeFlattenedPath = relativeFlattenedPath.resolve(dir);
                }
            }

            return relativeFlattenedPath;
        }

        private void getRelativeFlattenedPath(File relativeDir, IOFileFilter dirFlattenFilter, Deque<String> dirs) {
            if(!dirFlattenFilter.accept(relativeDir)) {
                dirs.addFirst(relativeDir.getName());
            }

            File parentDir = relativeDir.getParentFile();
            if(parentDir != null) {
                getRelativeFlattenedPath(parentDir, dirFlattenFilter, dirs);
            }
        }

        private Path getRelativeFlattenedPath(Path relativePath) {
            if(maxDirFlattenLvl > Options.NO_FLATTEN && !relativePath.toString().equals("")) {
                if(maxDirFlattenLvl == Options.FLATTEN_ALL_DIRS) {
                    relativePath = Paths.get("");
                } else {
                    int nameCt = relativePath.getNameCount();
                    if(nameCt <= maxDirFlattenLvl) {
                        relativePath = Paths.get("");
                    } else {
                        relativePath = relativePath.subpath(maxDirFlattenLvl, nameCt);
                    }
                }
            }
            return relativePath;
        }

        private boolean toFlattenDir(Path dir, int currentDirLvl) {
            // See if we are supposed to flatten all directories at this level. If the flatten filter is null or the flatten filter
            // returns false AND the maxDirFlattenLevel is less than or equal to the current dir level or equal to Options.NO_FLATTEN,
            // do not flatten the directory.
            // If the flatten filter returns true OR the maxDirFlattenLevel is Options.FLATTEN_ALL_DIRS or is greater than the current
            // dir level, flatten the directory.
            boolean flatten = options.getDirFlattenFilter().accept(dir.toFile());
            return flatten || (options.getMaxDirFlattenLevel() == Options.FLATTEN_ALL_DIRS || options.getMaxDirFlattenLevel() > currentDirLvl);
        }

        private boolean toMergeDir(Path dir, int currentDirLvl) {
            // See if we are supposed to merge all directories at this level. If the merge filter is null or the merge filter returns
            // false AND the maxDirMergeLevel is less than or equal to the current dir level or equal to Options.NO_MERGE, do not merge
            // the directory.
            // If the merge filter returns true OR the maxDirMergeLevel is Options.MERGE_ALL_DIRS or is greater than the current dir
            // level, merge the directory.
            boolean merge = options.getDirMergeFilter().accept(dir.toFile());
            return merge || (options.getMaxDirMergeLevel() == Options.MERGE_ALL_DIRS || options.getMaxDirMergeLevel() > currentDirLvl);
        }

        /**
         * Private method that determines if the current directory level should be
         * copied
         * @param curDirLevel current directory level
         * @return true if directory should be copied
         */
        private boolean toCopyDir(Path dir, int curDirLevel) {
            // See if we are supposed to copy all directories at this level. If the copy filter is null or the copy filter
            // return true AND the maxDirCopyLevel is Options.COPY_ALL_DIRS or the maxDirCopyLevel is greater than the
            // current dir level, copy the directory.
            // If the copy filter returns false OR the maxDirCopyLevel is less than or equal to the current dir level, do not
            // copy the directory.
            boolean copy = toCopyFile(dir);
            return copy && (options.getMaxDirCopyLevel() == Options.COPY_ALL_DIRS || options.getMaxDirCopyLevel() > curDirLevel);
        }

        /**
         * Returns true if the file passed in gets through the filecopy filter.
         * @param file
         * @return
         */
        private boolean toCopyFile(Path file) {
            return options.getFileCopyFilter().accept(file.toFile());
        }
    }

}
